﻿using System;
using System.Collections.Generic;
using System.Text;

using System.Net.Sockets;
using System.Net;
using System.IO;
using System.Security.Cryptography;
using System.Threading;
using System.Timers;
using System.Diagnostics;
using FileTransfer.BlockCipher;

namespace FileTransfer
{
    #region Event

    public delegate void LogEventHandler(object sender, LogEventArgs e);

    public class LogEventArgs : EventArgs
    {
        public LogEventArgs()
        {
            Status = LogEventStatus.Normal;
        }

        public string MessageLog { get; set; }

        public LogEventStatus Status { get; set; }
    }

    [Flags]
    public enum LogEventStatus
    {
        None = 0,
        Normal = 1 << 0,
        Good = 1 << 1,
        Bad = 1 << 2,
    }

    #endregion


    class FileReceiver
    {
        #region Event

        public event LogEventHandler LogGenerated;


        private void OnLogGenerated(object sender, LogEventArgs e)
        {
            if (LogGenerated != null)
            {
                LogGenerated(sender, e);
            }
        }

        #endregion



        #region Props

        // the size of the key of RSA-system
        public int KeySize { get; set; }

        // IP address and port of the current TCP server
        public IPAddress IpAddress { get; set; }
        public int Port { get; set; }


        public int FileSize { get; private set; }
        public string FileName { get; private set; }

        private Algorithm algorithm = Algorithm.None;

        #endregion


        #region Defaults

        // set as the key size when it is not specified
        private static readonly int defaultKeySize = 2048;

        // a range that used for port number to choose from
        private static readonly int minPort = 24000;
        private static readonly int maxPort = 25000;


        #endregion



        #region Constructors

        public FileReceiver(int keySize, IPAddress ipAddress, int port)
        {
            this.KeySize = keySize;
            this.IpAddress = ipAddress;
            this.Port = port;

            //Start();
        }

       
        public FileReceiver(int keySize, int port) :
            this(keySize, IPAddress.Any, port) { }


        public FileReceiver(int port)
            : this(defaultKeySize, port)
        {
            
        }
        public FileReceiver(IPAddress ipAddress, int port) :
            this(defaultKeySize, ipAddress, port) { }

        public FileReceiver()
            : this(defaultKeySize) {
                this.Port = new Random((int)DateTime.Now.Ticks).Next(minPort, maxPort);
            }


        #endregion



        #region Common

        private TcpListener server;

        private volatile bool isRunning;
        Thread listeningThread;

        public void Start()
        {
            try
            {
                server = new TcpListener(this.IpAddress, this.Port);

                server.Start();

                isRunning = true;

                listeningThread = new Thread(ListeningThread);
                listeningThread.Start();
            }
            catch (SocketException e)
            {
                OnLogGenerated(this, new LogEventArgs
                {
                    MessageLog = string.Format("SocketException: {0}", e.Message),
                    Status = LogEventStatus.Bad
                });
            }
            finally
            {
                // Stop listening for new clients.
                server.Stop();
            }

        }
        void ListeningThread()
        {
            OnLogGenerated(this, new LogEventArgs
            {
                MessageLog = "Waiting for a client request to transfer file...",
                Status = LogEventStatus.Normal
            });

            server.Start();

            // Enter the listening loop.
            while (isRunning)
            {
                
                
                if (!server.Pending())
                {
                    Thread.Sleep(100);
                    continue;
                }
                // Perform a blocking call to accept requests.
                using (TcpClient client = server.AcceptTcpClient())
                {
                    byte[] fileContent = ReceiveFile(client);

                    client.Close();
                }
            }
        }

        public void Stop()
        {
            isRunning = false;
        }

        private byte[] ReceiveFile(TcpClient client)
        {
            NetworkStream stream = client.GetStream();
            try
            {
                // get the uploaded file's name
                FileName = GetRequestForFile(stream);

                if (FileName == null) return null;

                // generate RSA public/private key-pair
                string publicRSAKeyXML = GenerateAsymmetricKeys();

                if (publicRSAKeyXML == null) return null;

                // and send public key
                SendPublicKey(publicRSAKeyXML, stream);


                // receive encrypted symmetric key
                byte[] encryptedSymKey = GetSymmetricKey(stream);

                if (encryptedSymKey == null) return null;



                // receive encrypted file
                byte[] encryptedFileContent = GetEncryptedData(stream);

                if (encryptedFileContent == null) return null;

                //SaveToFile(FileName + "ENCRYPTED", encryptedFileContent);


                // decrypt the key
                byte[] decryptedSymKey = DecryptSymmetricKey(encryptedSymKey);

                // decrypt the file
                byte[] decryptedData = DecryptData(encryptedFileContent, decryptedSymKey);


                SaveToFile(FileName, decryptedData);



                return decryptedData;
            }
            catch (Exception e)
            {
                OnLogGenerated(this, new LogEventArgs
                {
                    MessageLog = string.Format("Error while file receive: {0}", e),
                    Status = LogEventStatus.Bad
                });
            }
            finally
            {
                stream.Close();
            }
            return null;
        }

        private void SaveToFile(string fileName, byte[] fileData)
        {
            string pathToDownloadFolder = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.Personal), "SecureDownloads");
            if (!Directory.Exists(pathToDownloadFolder))
                Directory.CreateDirectory(pathToDownloadFolder);

            string pathToFile = Path.Combine(pathToDownloadFolder, Path.GetFileName(fileName));

            File.WriteAllBytes(pathToFile, fileData);
            OnLogGenerated(this, new LogEventArgs
            {
                MessageLog = "File was saved",
                Status = LogEventStatus.Bad
            });
        }

        #endregion





        #region Network functions


        private string GetRequestForFile(NetworkStream clientStream)
        {
            StreamReader readStream = new StreamReader(clientStream);
            string readString = readStream.ReadLine();

            if (!readString.Contains(SecureFileTransferProtocol.clFileSend)) return null;

            FileName = readString.Replace(SecureFileTransferProtocol.clFileSend, string.Empty);



            readString = readStream.ReadLine();

            if (!readString.Contains(SecureFileTransferProtocol.clFileSizeSend)) return null;

            string fileSizeString = readString.Replace(SecureFileTransferProtocol.clFileSizeSend, string.Empty);
            


            int fileSize = 0;
            if (!Int32.TryParse(fileSizeString, out fileSize)) return null;
            this.FileSize = fileSize;

            int alg = readStream.Read();
            this.algorithm = (Algorithm) Enum.ToObject(typeof(Algorithm), alg);

            OnLogGenerated(this, new LogEventArgs
            {
                MessageLog = string.Format("Get request to transfer file {0} with size {1} bytes using algorithm {2}", FileName, FileSize, Enum.GetName(typeof(Algorithm),this.algorithm)),
                Status = LogEventStatus.Bad
            });
            return FileName;
        }

        private void SendPublicKey(string publicRSAKeyXML, NetworkStream clientStream)
        {
            List<byte> rawData = new List<byte>(
                publicRSAKeyXML.Length +
                //SecureFileTransferProtocol.clSymKey.Length +
                SecureFileTransferProtocol.srvKeyLength.Length);


            rawData.AddRange(Encoding.ASCII.GetBytes(SecureFileTransferProtocol.srvKeyLength));
            rawData.AddRange(Encoding.ASCII.GetBytes(publicRSAKeyXML.Length.ToString()));
            rawData.AddRange(Encoding.ASCII.GetBytes(SecureFileTransferProtocol.delimeter));
            //rawData.AddRange(Encoding.ASCII.GetBytes(SecureFileTransferProtocol.clSymKey));
            rawData.AddRange(Encoding.ASCII.GetBytes(publicRSAKeyXML));

            clientStream.Write(rawData.ToArray(), 0, rawData.Count);

            OnLogGenerated(this, new LogEventArgs
            {
                MessageLog = string.Format("Send public key: \n{0}", publicRSAKeyXML),
                Status = LogEventStatus.Bad
            });

        }

        private byte[] GetSymmetricKey(NetworkStream clientStream)
        {
            StreamReader readStream = new StreamReader(clientStream);
            string readString = readStream.ReadLine();

            if (!readString.Contains(SecureFileTransferProtocol.clSymKeyLength)) return null;

            string keySizeString = readString.Replace(SecureFileTransferProtocol.clSymKeyLength, string.Empty);

            int keySize = 0;
            if (!Int32.TryParse(keySizeString, out keySize)) return null;


            BinaryReader br = new BinaryReader(clientStream, Encoding.ASCII);
            byte[] encryptedData = br.ReadBytes(keySize);
            OnLogGenerated(this, new LogEventArgs
            {
                MessageLog = "Received symmetric key",
                Status = LogEventStatus.Bad
            });
            return encryptedData;
        }

        byte padded = 0;
        private byte[] GetEncryptedData(NetworkStream clientStream)
        {
            byte[] rawData = new byte[this.FileSize + 100];
            int offset = 0;
            int read = -1;
            while (read != 0)
            {
                int rest = rawData.Length - offset;

                read = clientStream.Read(rawData, offset, Math.Min(1024, rest));
                offset += read;
                // Console.WriteLine(offset);
            }

            string readString = Encoding.ASCII.GetString(rawData, 0, offset);

            if (!readString.Contains(SecureFileTransferProtocol.clEncryptedFileLength)) return null;

            string fileSizeString = readString.Remove(readString.IndexOf(SecureFileTransferProtocol.delimeter)).Replace(SecureFileTransferProtocol.clEncryptedFileLength, string.Empty);


            int fileSize = 0;
            if (!Int32.TryParse(fileSizeString, out fileSize)) return null;

            padded = (byte)((8 - (byte)(fileSize % 8)) % 8);

            byte[] encryptedData = new byte[fileSize + padded];
            int startIndex = readString.IndexOf(SecureFileTransferProtocol.delimeter) + SecureFileTransferProtocol.delimeter.Length;
            Array.Copy(rawData, startIndex, encryptedData, 0, fileSize + padded);

            OnLogGenerated(this, new LogEventArgs
            {
                MessageLog = "Received encrypted file with " + padded.ToString() + " padded bytes",
                Status = LogEventStatus.Bad
            });


            return encryptedData;
        }

        #endregion


        #region Crypto

        private RSAParameters rsaParameters;


        private string GenerateAsymmetricKeys()
        {
            RSACryptoServiceProvider rsa = new RSACryptoServiceProvider(this.KeySize);

            // generate the key and save all the parameters for future using while decrypting
            this.rsaParameters = rsa.ExportParameters(true);

            // generate xml string with public key information to send the server
            return rsa.ToXmlString(false);
        }


        private byte[] DecryptSymmetricKey(byte[] encryptedSymKey)
        {
            var rsa = new RSACryptoServiceProvider();

            rsa.ImportParameters(rsaParameters);

            return rsa.Decrypt(encryptedSymKey, false);
        }


        private byte[] DecryptData(byte[] data, byte[] symKey)
        {
            if (symKey.Length <= 0) throw new ArgumentException("The size of symmetric key must be greater than zero");

            var sw = new Stopwatch(); 
            sw.Start();
            
            
            BlockCipher.BlockCipher encrypter;

            switch (algorithm)
            {
                case Algorithm.GOST:
                    encrypter = new GOST_89(symKey);
                    break;
                case Algorithm.Blowfish:
                    encrypter = new Blowfish(symKey);
                    break;
                default:
                    return null;
            }

            byte[] decryptedDataWithPad = encrypter.Decipher(data, padded);
            byte[] decryptedData = new byte[this.FileSize];
           
            Array.Copy(decryptedDataWithPad, decryptedData, this.FileSize);
            sw.Stop();
       
            OnLogGenerated(this, new LogEventArgs
            {
                MessageLog = string.Format("Decrypt the file in {0}", sw.Elapsed),
                Status = LogEventStatus.Bad
            });


            return decryptedData;
        }


        #endregion

    }
}
